package jmine.tec.hydra.domain;

import static org.hibernate.annotations.CascadeType.ALL;
import static org.hibernate.annotations.CascadeType.DELETE_ORPHAN;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;

import jmine.tec.hydra.HydraController;
import jmine.tec.hydra.HydraMessages;
import jmine.tec.hydra.api.HydraAction;
import jmine.tec.hydra.api.HydraMessage;
import jmine.tec.hydra.api.HydraRequest;
import jmine.tec.hydra.api.HydraRequestVisitor;
import jmine.tec.hydra.api.HydraSpecification;
import jmine.tec.hydra.dao.HydraConversationDAO;
import jmine.tec.hydra.dao.HydraMessageImplDAO;
import jmine.tec.hydra.domain.actions.AbstractHydraAction;
import jmine.tec.hydra.domain.enumx.HydraRequestType;
import jmine.tec.hydra.domain.message.HydraConversation;
import jmine.tec.hydra.domain.specification.HydraSpecificationImpl;
import jmine.tec.hydra.exception.HydraException;
import jmine.tec.persist.annotation.Alias;
import jmine.tec.persist.annotation.Constraint;
import jmine.tec.persist.annotation.Index;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.Type;
import org.hibernate.validator.NotNull;

import bancosys.tec.component.Documentation;
import bancosys.tec.exception.BusinessException;
import bancosys.tec.persist.bussobj.PersistableBusinessObject;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.rtm.impl.exception.dao.ExceptionRecordDAO;
import bancosys.tec.rtm.impl.exception.domain.ExceptionRecord;
import bancosys.tec.utils.date.Timestamp;

/**
 * Classe de dominio para o request do hydra
 * 
 * @author george.queiroz dez/2009
 */
@Entity
@Table(name = "HYDRA_REQUEST")
@Alias("HYDREQ")
@SequenceGenerator(name = "SEQ_HYDREQ", sequenceName = "SEQ_HYDREQ")
@Documentation("ARMAZENA OS REGISTROS DE REQUISICAO AO HYDRA.")
public class HydraRequestImpl extends PersistableBusinessObject implements HydraRequest {

    private static final Log LOG = LogFactory.getLog(HydraRequest.class);

    private static final int MAX_SUMMARY_LENGTH = 255;

    private Long id;

    private Timestamp requestStart;

    private Timestamp requestEnd;

    private StatusHydraRequest status;

    private Set<HydraAction> requestActions = new LinkedHashSet<HydraAction>();

    private ExceptionRecord exceptionRecord;

    private Long exceptionRecordId;

    private String user;

    private HydraRequestType requestType;

    private HydraSystem system;

    private HydraSpecification specification;

    private List<? extends HydraMessage> requestMessages;

    private String summary;

    // Transient
    private List<HydraAction> transientActions;

    /**
     * {@inheritDoc}
     */
    public HydraRequest execute(HydraAction act) {
        try {
            this.initialize();
            this.doExecute(act);
            this.success();
            return this;
        } catch (HydraException e) {
            this.failure(e);
            e.setRequest(this);
            throw e;
        } catch (BusinessException e) {
            this.failure(e);
            throw e;
        } catch (Throwable t) {
            HydraException e = new HydraException(HydraMessages.REQUEST_ERROR.create(t.getMessage()), t);
            this.failure(e);
            e.setRequest(this);
            throw e;
        }
    }

    /**
     * Inicializa o requeste, salvando-o
     * 
     * @throws Exception Exception
     */
    private void initialize() throws Exception {
        this.setStatus(StatusHydraRequest.INITIALIZED);
        this.setRequestStart(this.getController().getClock().currentTimestamp());
        this.setUser(this.getController().getSecurityService().getCurrentUser());
    }

    /**
     * Efetua a finalização do request para o caso de uma execução normal aos olhos do HydraRequest. Podem existir etapas com falhas ou
     * pendentes.
     * 
     * @throws Exception Exception
     */
    private void success() throws Exception {
        if (this.getStatus().initialized()) {
            this.setStatus(StatusHydraRequest.SUCCESS);
        }
        if (this.getSummary() == null) {
            this.setSummary("Sucesso.");
        }
        this.setRequestEnd(this.getController().getClock().currentTimestamp());
        if (this.getId() == null) {
            HydraRequestImpl.this.applyTransientAction();
            LOG.debug("Requisição hydra terminada sem que nenhuma ação relevante tenha sido tomada.");
        } else {
            this.executeTransacted(new Callable<Void>() {
                public Void call() throws Exception {
                    HydraRequestImpl.this.applyTransientAction();
                    HydraRequestImpl.this.getPersister().save();
                    return null;
                }
            });
        }
    }

    /**
     * Tenta salvar o hydraRequest no caso de uma falha crítica. Tenta salvar a exception juntamente, caso não seja possível pois ocorreu
     * uma exception monitora utilizando o RTM. Este método não deve jogar exception nunca.
     * 
     * @param t the {@link Throwable}
     */
    private void failure(final Throwable t) {
        this.setStatus(StatusHydraRequest.FAILED);
        this.setRequestEnd(this.getController().getClock().currentTimestamp());
        if (this.getSummary() == null) {
            this.setSummary(t.getMessage());
        }
        try {
            this.executeTransacted(new Callable<Void>() {
                public Void call() throws Exception {
                    HydraRequestImpl.this.applyTransientAction();
                    HydraRequestImpl.this.storeException(t);
                    HydraRequestImpl.this.getPersister().save();
                    return null;
                }
            });
        } catch (Throwable throwable) {
            this.getController().getRtmController().monitor(throwable);
            this.getController().getRtmController().monitor(t);
        }
    }

    /**
     * Passa as actions transientes que foram criadas durante a execução da requisição para o conjunto de ações persistentes.
     */
    protected void applyTransientAction() {
        this.requestActions.addAll(this.transientActions);
    }

    /**
     * Efetua a execução do request, executando todas as actions.
     * 
     * @param action action
     * @throws Throwable caso uma falha crítica ocorra
     */
    private void doExecute(HydraAction action) throws Throwable {
        this.transientActions = new ArrayList<HydraAction>();
        this.transientActions.add(action);
        HydraRequestVisitor visitor = this.createVisitor();
        int executed = 0;
        while (this.transientActions.size() > executed) {
            HydraAction current = this.transientActions.get(executed++);
            this.transientActions.addAll(current.execute(this, visitor));
        }
    }

    /**
     * @return HydraRequestVisitor
     */
    protected HydraRequestVisitor createVisitor() {
        return new HydraRequestImplVisitor();
    }

    /**
     * Tenta armazenar a exception. Não deve falhar, faz o possível para guardar o máximo de informação da exception. Caso não seja possível
     * salvar o ExceptionRecord, envia para o RTM monitor.
     * 
     * @param t t
     */
    private void storeException(Throwable t) {
        try {
            this.setExceptionRecord(HydraRequestImpl.this.getController().getRtmController().persist(t));
        } catch (Throwable t2) {
            // Primeiro monitorar o erro que impediu que o ExceptionRecord fosse gerado
            this.getController().getRtmController().monitor(t2);
        }
    }

    /**
     * Executa um bloco callable dentro de uma transação, efetuando commit caso não ocorra falha e dando rollback caso tenha ocorrido.
     * 
     * @param <V> tipo de retorno do callable
     * @param transactionalAction transactionalAction
     * @return retorno do callable
     * @throws Exception Exception
     */
    private <V> V executeTransacted(Callable<V> transactionalAction) throws Exception {
        return this.getController().getPersistenceController().executeTransacted(transactionalAction);
    }

    /**
     * {@inheritDoc}
     */
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_HYDREQ")
    @Column(name = "COD_HYDRA_REQUEST", nullable = false)
    @Documentation("CODIGO DO REQUEST DO HYDRA.")
    public Long getId() {
        return this.id;
    }

    /**
     * {@inheritDoc}
     */
    @Column(name = "REQUEST_START", nullable = false)
    @Type(type = "bancosys.tec.persist.hibernate.type.TimestampType")
    @Documentation("TEMPO DE INICIO DO REQUEST.")
    public Timestamp getRequestStart() {
        return this.requestStart;
    }

    /**
     * {@inheritDoc}
     */
    @Column(name = "REQUEST_END")
    @Type(type = "bancosys.tec.persist.hibernate.type.TimestampType")
    @Documentation("TEMPO EM QUE O REQUEST FOI FINALIZADO.")
    public Timestamp getRequestEnd() {
        return this.requestEnd;
    }

    /**
     * {@inheritDoc}
     */
    @Column(name = "STATUS", nullable = false)
    @Documentation("INDICA O STATUS DA REQUISICAO DO HYDRA: 0-INICIADO, 1-SUCESSO, 2-FALHA, 3 - PENDENTE, 4 - RESOLVIDO")
    @Constraint(suffix = "0")
    public StatusHydraRequest getStatus() {
        return this.status;
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    public List<HydraComment> getComments() {
        List<HydraComment> comments = new ArrayList<HydraComment>();
        for (HydraAction act : this.getActions()) {
            comments.addAll(act.getComments());
        }
        return comments;
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    // XXX (mmr) : relacionamento sem FK para permitir expurgo de ExecutionLog de forma independente de relacao com o Hydra
    public ExceptionRecord getExceptionRecord() {
        if (this.exceptionRecord == null && this.exceptionRecordId != null) {
            ExceptionRecordDAO dao = this.getController().getDAOFactory().getDAOByClass(ExceptionRecordDAO.class);
            try {
                this.exceptionRecord = dao.findByPk(this.exceptionRecordId);
            } catch (BeanNotFoundException e) {
                // do nothing
            }
        }
        return this.exceptionRecord;
    }

    /**
     * @return valor do campo exceptionRecordId.
     */
    @Index(suffix = "JT_0")
    @Column(name = "COD_EXCEPTION")
    @Documentation("CODIGO DOS ERROS (EXCEPTION) OCORRIDO ENTRE OS SISTEMA INTEGRADOS.")
    protected Long getExceptionRecordId() {
        return this.exceptionRecordId;
    }

    /**
     * Define valor para campo exceptionRecordId.
     * 
     * @param exceptionRecordId valor para campo exceptionRecordId.
     */
    protected void setExceptionRecordId(Long exceptionRecordId) {
        this.exceptionRecordId = exceptionRecordId;
    }

    /**
     * {@inheritDoc}
     */
    @NotNull
    @Column(name = "USU_REQ", nullable = false)
    @Documentation("USUARIO QUE INICIOU A EXECUÇÃO DA REQUISIÇÃO")
    public String getUser() {
        return this.user;
    }

    /**
     * @return the requestType
     */
    @NotNull
    @Constraint(suffix = "JT_0")
    @Column(name = "TP_REQ", nullable = false)
    @Documentation("TIPO DE REQUISICAO. 0 - ENVIO, 1 - RECEBIMENTO")
    public HydraRequestType getRequestType() {
        return this.requestType;
    }

    /**
     * @return the system
     */
    @ManyToOne
    @Index(suffix = "JT_2")
    @JoinColumn(name = "COD_HYDRA_SYSTEM")
    @Documentation("CODIGO DO SISTEMA ASSOCIADO A REQUISICAO")
    public HydraSystem getSystem() {
        return this.system;
    }

    /**
     * @return the specification
     */
    @ManyToOne(targetEntity = HydraSpecificationImpl.class)
    @Index(suffix = "JT_3")
    @JoinColumn(name = "COD_HYDRA_SPEC")
    @Documentation("CODIGO DA ESPECIFICACAO DISPARADA PELA REQUISICAO.")
    public HydraSpecification getSpecification() {
        return this.specification;
    }

    /**
     * @return the message
     */
    @Transient
    protected List<? extends HydraMessage> getRequestMessages() {
        if (this.requestMessages == null) {
            HydraMessageImplDAO dao = this.getController().getDAOFactory().getDAOByClass(HydraMessageImplDAO.class);
            this.requestMessages = dao.findByRequest(this);
        }
        return this.requestMessages;
    }

    /**
     * @return the summary
     */
    @Column(name = "REQUEST_SUMMARY", length = MAX_SUMMARY_LENGTH)
    @Documentation("RESUMO DA REQUISICAO.")
    public String getSummary() {
        return this.summary;
    }

    /**
     * @param id the id to set
     */
    @SuppressWarnings("unused")
    private void setId(Long id) {
        this.id = id;
    }

    /**
     * @param requestStart the requestStart to set
     */
    protected void setRequestStart(Timestamp requestStart) {
        this.requestStart = requestStart;
    }

    /**
     * @param requestEnd the requestEnd to set
     */
    protected void setRequestEnd(Timestamp requestEnd) {
        this.requestEnd = requestEnd;
    }

    /**
     * @param status the status to set
     */
    protected void setStatus(StatusHydraRequest status) {
        this.status = status;
    }

    /**
     * @param exceptionRecord the exceptionRecord to set
     */
    protected void setExceptionRecord(ExceptionRecord exceptionRecord) {
        this.exceptionRecord = exceptionRecord;
        this.exceptionRecordId = exceptionRecord.getId();
    }

    /**
     * @param user the user to set
     */
    protected void setUser(String user) {
        this.user = user;
    }

    /**
     * @param requestType the requestType to set
     */
    public void setRequestType(HydraRequestType requestType) {
        this.requestType = requestType;
    }

    /**
     * @param system the system to set
     */
    public void setSystem(HydraSystem system) {
        this.system = system;
    }

    /**
     * @param specification the specification to set
     */
    public void setSpecification(HydraSpecification specification) {
        this.specification = specification;
    }

    /**
     * @param summary the summary to set
     */
    protected void setSummary(String summary) {
        this.summary = StringUtils.abbreviate(summary, MAX_SUMMARY_LENGTH);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Request id: ");
        sb.append(this.id);
        return sb.toString();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transient
    public HydraController getController() {
        return (HydraController) super.getController();
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    public List<HydraAction> getActions() {
        List<HydraAction> list = new ArrayList<HydraAction>(this.getRequestActions());
        Collections.sort(list, new Comparator<HydraAction>() {
            public int compare(HydraAction o1, HydraAction o2) {
                return o1.getActionStart().compareTo(o2.getActionStart());
            }
        });
        return list;
    }

    /**
     * @return requestActions
     */
    @OneToMany(mappedBy = "request", fetch = FetchType.LAZY, targetEntity = AbstractHydraAction.class)
    @Cascade(value = { ALL, DELETE_ORPHAN })
    protected Set<HydraAction> getRequestActions() {
        return this.requestActions;
    }

    /**
     * @param requestActions requestActions
     */
    protected void setRequestActions(Set<HydraAction> requestActions) {
        this.requestActions = requestActions;
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    public List<HydraMessage> getMessages() {
        List<HydraMessage> messages = new ArrayList<HydraMessage>(this.getRequestMessages());
        Collections.sort(messages, new Comparator<HydraMessage>() {
            public int compare(HydraMessage o1, HydraMessage o2) {
                return o1.getId().compareTo(o2.getId());
            }
        });
        return messages;
    }

    /**
     * {@inheritDoc}
     */
    public void markPendingActionSolved(HydraAction action) {
        boolean requestSolved = true;
        for (HydraAction requestAction : this.getRequestActions()) {
            if (requestAction.getPk().equals(action.getPk())) {
                requestAction.markSolved();
            } else if (requestAction.getStatus().pending()) {
                requestSolved = false;
            }
        }
        if (requestSolved) {
            this.setStatus(StatusHydraRequest.SOLVED);
        }
        this.getPersister().save();
    }

    /**
     * Visitor para que actions possam interagir com a requisição durante sua execução.
     * 
     * @author lundberg
     */
    private class HydraRequestImplVisitor implements HydraRequestVisitor {
        /**
         * Marca o status como pendente
         */
        public void markAsPending() {
            HydraRequestImpl.this.status = StatusHydraRequest.PENDING;
        }

        /**
         * {@inheritDoc}
         */
        public void setSummary(String summary) {
            HydraRequestImpl.this.setSummary(summary);
        }

        /**
         * {@inheritDoc}
         */
        public void setSystem(HydraSystem system) {
            HydraRequestImpl.this.setSystem(system);
        }

        /**
         * {@inheritDoc}
         */
        public void setSpecification(HydraSpecification specification) {
            HydraRequestImpl.this.setSpecification(specification);
        }

        /**
         * Salva a mensagem, associando com a conversa, criando a conversa caso necessário.
         * 
         * @param identification identification
         * @param message message
         */
        public void saveMessageWithConversation(final String identification, final HydraMessage message) {
            try {
                HydraRequestImpl.this.executeTransacted(new Callable<Void>() {
                    public Void call() throws Exception {
                        HydraRequestImplVisitor.this.addMessage(message);
                        HydraConversation conversation = HydraRequestImplVisitor.this.findOrCreateConversation(identification);
                        message.setConversation(conversation);
                        conversation.addMessage(message);
                        message.getPersister().save();
                        HydraRequestImpl.this.getPersister().save();
                        conversation.getPersister().save();
                        Hibernate.initialize(conversation.getMessages());
                        return null;
                    }
                });
                HydraRequestImpl.this.getController().getSessionFactory().getCurrentSession().refresh(message);
            } catch (Exception e) {
                throw new HydraException(HydraMessages.REQUEST_ERROR.create(e.getMessage()), e);
            }
        }

        /**
         * Salva uma mensagem, em uma transação própria.
         * 
         * @param message message
         */
        public void saveMessage(final HydraMessage message) {
            try {
                HydraRequestImpl.this.executeTransacted(new Callable<Void>() {
                    public Void call() throws Exception {
                        HydraRequestImplVisitor.this.addMessage(message);
                        message.getPersister().save();
                        HydraRequestImpl.this.getPersister().save();
                        return null;
                    }
                });
                // getController().getSessionFactory().getCurrentSession().refresh(message);
            } catch (Exception e) {
                throw new HydraException(HydraMessages.REQUEST_ERROR.create(e.getMessage()), e);
            }
        }

        /**
         * Busca ou cria uma conversa com a identificação dada. Conversation retornada persistida sempre.
         * 
         * @param identification identification
         * @return HydraConversation
         */
        private HydraConversation findOrCreateConversation(String identification) {
            HydraConversationDAO dao = HydraRequestImpl.this.getController().getDAOFactory().getDAOByClass(HydraConversationDAO.class);
            if (StringUtils.isBlank(identification)) {
                return this.createConversation(identification);
            } else {
                try {
                    return dao.findByNaturalKey(identification);
                } catch (BeanNotFoundException e) {
                    return this.createConversation(identification);
                }
            }
        }

        /**
         * Cria uma conversa com a identificação dada.
         * 
         * @param identification identification
         * @return HydraConversation
         */
        private HydraConversation createConversation(String identification) {
            HydraConversationDAO dao = HydraRequestImpl.this.getController().getDAOFactory().getDAOByClass(HydraConversationDAO.class);
            HydraConversation conversation = dao.createBean();
            conversation.setSpecification(HydraRequestImpl.this.getSpecification());
            conversation.setMnemonico(StringUtils.isBlank(identification) ? HydraRequestImpl.this.getController().createIdentification()
                    : identification);
            conversation.getPersister().save();
            return conversation;
        }

        /**
         * Adiciona a mensagem à lista de mensagens da requisição, salvando a requisição caso não esteja salva.
         * 
         * @param message message
         */
        private void addMessage(HydraMessage message) {
            if (HydraRequestImpl.this.getId() == null) {
                HydraRequestImpl.this.getPersister().save();
            }
            message.setRequest(HydraRequestImpl.this);
        }
    }

}
